package control

import (
	"context"
	"net"
	"recorder/log"
	"time"

	"github.com/gorilla/websocket"
)

const (
	messageSizeMax = 1000 * 1000
	readTimeout    = 10 * time.Second
	writeTimeout   = 10 * time.Second
	pingTimeout    = 5 * time.Second
	closeTimeout   = 1 * time.Second
)

type Conn struct {
	ws   *websocket.Conn
	done chan struct{}
	id   string
	Out  chan *Message
	In   chan *Message
	Dis  chan struct{}
}

func NewConn(ws *websocket.Conn, id string) *Conn {
	c := &Conn{
		ws:   ws,
		done: make(chan struct{}),
		id:   id,
		Out:  make(chan *Message),
		In:   make(chan *Message),
		Dis:  make(chan struct{}),
	}
	return c
}

func (c *Conn) Run() {
	log.Error("start run step 1")
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go c.writeLoop(ctx)
	c.readLoop()
}

func (c *Conn) Close() {
	close(c.done)
}

func (c *Conn) Id() string {
	return c.id
}

func (c *Conn) LocalAddr() net.Addr {
	return c.ws.LocalAddr()
}

func (c *Conn) RemoteAddr() net.Addr {
	return c.ws.RemoteAddr()
}

func (c *Conn) readLoop() {
	defer close(c.Dis)
	defer close(c.In)
	defer c.ws.Close()
	c.ws.SetReadLimit(messageSizeMax)
	c.ws.SetPongHandler(func(appData string) error {
		c.ws.SetReadDeadline(time.Now().Add(readTimeout))
		return nil
	})
	for {
		log.Error("start read step 1")
		c.ws.SetReadDeadline(time.Now().Add(readTimeout))
		log.Error("start read step 2")

		var m Message
		if e := c.ws.ReadJSON(&m); e != nil {
			if websocket.IsUnexpectedCloseError(e, websocket.CloseGoingAway) {
				log.Error("conn %s<-%s read err %s", c.ws.LocalAddr(), c.ws.RemoteAddr(), e.Error())
			}
			log.Error("start error ", e)
			return
		}
		log.Error("start read step 3")

		log.Info("conn %s<-%s recv %+v", c.ws.LocalAddr(), c.ws.RemoteAddr(), m)
		select {
		case <-c.done:
		case c.In <- &m:
		}
	}
}

func (c *Conn) writeLoop(ctx context.Context) {
	ping := time.NewTicker(pingTimeout)
	defer ping.Stop()
	defer close(c.Out)
	for {
		select {
		case <-c.done:
			log.Warning("conn %s->%s closing", c.ws.LocalAddr(), c.ws.RemoteAddr())
			c.ws.SetWriteDeadline(time.Now().Add(writeTimeout))
			if e := c.ws.WriteMessage(websocket.CloseGoingAway, []byte{}); e != nil {
				log.Warning("conn %s->%s closed", c.ws.LocalAddr(), c.ws.RemoteAddr())
				return
			}
			select {
			case <-ctx.Done():
			case <-time.After(closeTimeout):
			}
			log.Warning("conn %s->%s closed", c.ws.LocalAddr(), c.ws.RemoteAddr())
			return
		case m := <-c.Out:
			log.Info("conn %s->%s send %+v", c.ws.LocalAddr(), c.ws.RemoteAddr(), m)
			c.ws.SetWriteDeadline(time.Now().Add(writeTimeout))
			if e := c.ws.WriteJSON(m); e != nil {
				log.Warning("conn %s->%s send error: %s", c.ws.LocalAddr(), c.ws.RemoteAddr(), e.Error())
			}
		case <-ping.C:
			c.ws.SetWriteDeadline(time.Now().Add(writeTimeout))
			if e := c.ws.WriteMessage(websocket.PingMessage, []byte{}); e != nil {
				log.Warning("conn %s->%s ping err %s", c.ws.LocalAddr(), c.ws.RemoteAddr(), e.Error())
			}
		}
	}
}
